Skip to content

fix(components): Fix autocomplete crashing on resize larger [JOB-151844]#2924

Merged
Aiden-Brine merged 7 commits intomasterfrom
JOB-151844/max-depth-error
Mar 5, 2026
Merged

fix(components): Fix autocomplete crashing on resize larger [JOB-151844]#2924
Aiden-Brine merged 7 commits intomasterfrom
JOB-151844/max-depth-error

Conversation

@Aiden-Brine
Copy link
Contributor

@Aiden-Brine Aiden-Brine commented Feb 23, 2026

Motivations

Maximum depth errors popped up in DataDog related to Autocomplete. It was very difficult to re-create but we eventually figured out it could be recreated sometimes, for certain people, with a monitor connected, if they had the autocomplete menu open, and they expanded their screen size from smaller to larger. Here is a stack trace:

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    at getRootForUpdatedFiber (http://localhost:6007/node_modules/.cache/storybook/b124b7c47095b41d5ea618f285d096b0b3bcf3b8444e7fa72f6a80c348532f13/sb-vite/deps/chunk-HWACSCKG.js?v=2a05bdab:3001:128)
    at enqueueConcurrentHookUpdate (http://localhost:6007/node_modules/.cache/storybook/b124b7c47095b41d5ea618f285d096b0b3bcf3b8444e7fa72f6a80c348532f13/sb-vite/deps/chunk-HWACSCKG.js?v=2a05bdab:2985:16)
    at dispatchSetStateInternal (http://localhost:6007/node_modules/.cache/storybook/b124b7c47095b41d5ea618f285d096b0b3bcf3b8444e7fa72f6a80c348532f13/sb-vite/deps/chunk-HWACSCKG.js?v=2a05bdab:5350:20)
    at dispatchSetState (http://localhost:6007/node_modules/.cache/storybook/b124b7c47095b41d5ea618f285d096b0b3bcf3b8444e7fa72f6a80c348532f13/sb-vite/deps/chunk-HWACSCKG.js?v=2a05bdab:5321:9)
    at Object.setReference (http://localhost:6007/node_modules/.cache/storybook/b124b7c47095b41d5ea618f285d096b0b3bcf3b8444e7fa72f6a80c348532f13/sb-vite/deps/@floating-ui_react.js?v=88efab77:5570:7)
    at http://localhost:6007/src/Autocomplete/hooks/useAutocompleteListNav.ts:93:18
    at http://localhost:6007/src/Autocomplete/Autocomplete.rebuilt.tsx:104:7
    at http://localhost:6007/src/utils/mergeRefs.ts:5:9
    at Array.forEach (<anonymous>)
    at http://localhost:6007/src/utils/mergeRefs.ts:3:10

Changes

Fix for the crash

mergeRefs([...]) called inline creates a brand new callback function on every render of InputText. React tracks ref props by identity, if the ref prop is a different function than last render, React detaches the old one (calls it with null) then re-attaches the new one (calls it with the element). That detach chain ultimately calls refs.setReference(null) inside floating-ui, which calls setState. When this happened during floating-ui's flushSync call on window resize, it caused a nested synchronous React commit that exceeded the maximum update depth limit and crashed.

inputRef (our mergedInputRef from Autocomplete) and inputTextRef (an internal useRef, always stable) are both stable references. Memoizing with those as deps means mergedRef never changes identity, and React never schedules a detach/re-attach.

Upon fixing this though there were styling issues instead of the crash:

image

Fix for styling issues

Stable setReferenceElement

The refs object returned by useFloating can get a new object identity when floating-ui's internal state changes (e.g. after a position recalculation on resize). Since setReferenceElement listed refs as a dependency, it would get a new function identity whenever refs changed. That propagated up: referenceInputRef in Autocomplete.rebuilt.tsx depends on setReferenceElement, so it recreated too. Then mergedInputRef (which depends on referenceInputRef) recreated too. React then scheduled a detach/re-attach of the input ref, the same crash path described above.

The latestRefs pattern solves this by storing refs in a mutable useRef container that is updated synchronously every render. The setReferenceElement callback reads from the container at call time rather than capturing refs at creation time. Since a useRef container itself is always the same object identity, the dependency array can be empty, giving setReferenceElement a permanently stable identity.

size middleware sets width

Previously, the dropdown width was managed as React state (menuWidth), captured once when the input ref attached on mount via clientWidth. Because the above fixes made the ref stable, it no longer detaches and re-attaches on resize, which meant menuWidth was never refreshed after mount, the dropdown would show with a stale width after a screen resize.

The size middleware's apply callback runs on every autoUpdate trigger, so rects.reference.width is always the live, freshly-measured border-box width of the position reference element (the Form-Field-Wrapper). Moving width here eliminates the stale state entirely, using floating-ui's own recommended pattern for this purpose. It also removes the need for the menuWidth state variable and all associated code in Autocomplete.rebuilt.tsx.

Updating the visual regression snapshots

clientWidth is a DOM property that returns a rounded integer representing the element's content + padding width, explicitly excluding the border. Meanwhile, rects.reference.width comes from floating-ui's internal call to getBoundingClientRect().width. That returns a floating-point number representing the element's border-box width: content + padding + border.

The outcome of this is that the menu is 2px wider on both sides to include the border width. This is most obvious by using the onion view when inspecting the screenshots. As you slide from left to right you will see the menu increase in width slightly as it shifts from the old styles to the new styles. Otherwise, everything else should be functionally the same.

Testing

The component should look and behave the exact same as before except for no longer crashing on resize with the menu open.

Changes can be
tested via Pre-release


In Atlantis we use Github's built in pull request reviews.

@Aiden-Brine Aiden-Brine requested a review from a team as a code owner February 23, 2026 20:07
@Aiden-Brine Aiden-Brine marked this pull request as draft February 23, 2026 20:08
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 23, 2026

Deploying atlantis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 052e989
Status: ✅  Deploy successful!
Preview URL: https://e688c724.atlantis.pages.dev
Branch Preview URL: https://job-151844-max-depth-error.atlantis.pages.dev

View logs

@Aiden-Brine Aiden-Brine force-pushed the JOB-151844/max-depth-error branch from 0087dfe to 241f2e8 Compare February 26, 2026 00:22
@Aiden-Brine Aiden-Brine changed the title JOB_151844 Max depth error fix(components): Fix automcomplete crashing on resize larger [JOB-151844] Feb 27, 2026
@Aiden-Brine Aiden-Brine marked this pull request as ready for review February 27, 2026 16:58
[],
);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my first thought is if we could use useCallbackRef instead.

something like const setReferenceElement = useCallbackRef(() => refs.setReference);

I'll have to double check this is equivalent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useCallbackRef(() => refs.setReference) isn’t equivalent because it returns the function instead of invoking it with the element. I’d need useCallbackRef((el) => refs.setReference(el)), but for a ref callback useStableSetReference is safer since it keeps refs current during render rather than after commit (useEffect).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough, I'm happy to leave it as is then.

});
Object.assign(elements.floating.style, {
maxHeight: `${maxHeight}px`,
width: `${rects.reference.width}px`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clever! I forgot rects is available here. I like the colocation of dimension related setting code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice we used to have a maxWidth too. I can't recall why that was added... maybe it's not necessary with this setup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxWidth came from the old menuWidth workaround, and since we now set width from rects.reference.width in the floating size middleware (with the wrapper as the position reference), it seems redundant unless we intentionally want a different cap behavior.


const mergedRef = useMemo(
() => mergeRefs([inputRef, inputTextRef]),
[inputRef, inputTextRef],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these deps going to do anything? they're refs AKA objects, so React will just look at them and say "yup" that's the same object we had before. it won't look at the .current value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized the deps aren't needed and removed them in e34943b

@ZakaryH
Copy link
Contributor

ZakaryH commented Mar 2, 2026

I just merged the multiselect PR for Autocomplete which has created a couple merge conflicts 😅

the snapshots should be quick and easy. hopefully the main tsx file too. lmk if it gets complicated or you have any questions about the new setup at all!

@Aiden-Brine Aiden-Brine requested a review from ZakaryH March 2, 2026 23:59
@ZakaryH
Copy link
Contributor

ZakaryH commented Mar 3, 2026

still works great for single!

I did notice that on multiple, specifically when going from a smaller size to a larger one, this can happen

image

going from larger to smaller is fine.

this wasn't even available when you started working on the ticket so it's a net new scenario. that said, I'm not sure it's worth the extra effort. it's so niche.

I'll take one quick peek to see if there's a simple solution, and if not I think I'm fine go ahead with the solution as-is. it fixes the original issue 100% and accounts for 50% of a new scenario, where even when it's not doing the optimal thing - it's not breaking in some serious way like the original error.

@ZakaryH
Copy link
Contributor

ZakaryH commented Mar 3, 2026

ahh I see what it is. my multiple work brought back the thing you removed but it doesn't show as a merge conflict because it's in a new file!

in FloatingMenu.tsx we have the maxWidth and width assignment again. these can both go away along with the menuWidth prop on FloatingMenu and the React state/setter of menuWidth and setMenuWidth

that gets us back to your changes in the size middleware taking care of things, and we don't have the max width getting in the way anymore.

Copy link
Contributor

@ZakaryH ZakaryH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verified that on prod I'm still hitting the issue

Image

and now on this branch, for both single and multiple - it both no longer crashes and it correctly resizes in both directions. additionally, the resize logic is a bit cleaner in just one place now.

great fix

@Aiden-Brine Aiden-Brine changed the title fix(components): Fix automcomplete crashing on resize larger [JOB-151844] fix(components): Fix autocomplete crashing on resize larger [JOB-151844] Mar 5, 2026
@Aiden-Brine Aiden-Brine merged commit 4cce232 into master Mar 5, 2026
16 checks passed
@Aiden-Brine Aiden-Brine deleted the JOB-151844/max-depth-error branch March 5, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants